﻿///////////////////////////////////////////////////////////////////////////////
//
// This file is part of the BassDrop project
//   https://code.google.com/p/bass-drop/
//
//
// Creator:	Gergely Krajcsovszki (krajcsovszkig@hotmail.com)
// Date:	2013-07-24 19:00
//
//
// Copyright © 2013-2014. Gergely Krajcsovszki
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
//   http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
//
///////////////////////////////////////////////////////////////////////////////
using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Linq;
using System.Text;
using System.Text.RegularExpressions;
using BassDrop.Core.Common.Cloud;
using BassDrop.Core.Common.Cloud.Requests;
using BassDrop.Core.Common.Cloud.Requests.Assets;
using BassDrop.Core.Common.Data;
using BassDrop.Core.Common.Data.IO;
using BassDrop.Core.Common.Data.IO.Portable;
using BassDrop.Core.Common.General.Collections;
using BassDrop.Core.Common.Media.Playlists;
using BassDrop.Core.Common.Services;
using BassDrop.Core.Common.Utilities;

namespace BassDrop.Core.Common.Media.Library
{
	/// <summary>
	/// Media library
	/// </summary>
	public class Library : ISynchronizable, IDisposable, INotifyPropertyChanged
	{
		// TODO: class BassDrop.Core.Common.Media.Library.Library

		private static Library s_instance;

		/// <summary>
		/// Returns the singleton instance of the media library
		/// </summary>
		public static Library Instance
		{
			get
			{
				return s_instance;
			}
		}

		private object m_lock = new object();

		/// <summary>
		/// There is a synch request scheduled for the entries that hasn't started executing yet
		/// </summary>
		private volatile bool m_synchEntriesRequestScheduled = false;

		/// <summary>
		/// The synch request scheduled for the playlists that hasn't started executing yet
		/// </summary>
		private CloudRequestBase m_synchPlaylistsRequest = null;

		private volatile bool m_loadingPlaylists = false;

		/// <summary>
		/// The map of entries in the library
		/// </summary>
		private Dictionary<long, LibraryEntry> m_entries; // TODO: ez is inkább supercollection, esetleg keresés gyorsítására map is

		/// <summary>
		/// The list of playlists attached to the application
		/// </summary>
		public SuperCollection<Playlist> Playlists
		{
			get;
			private set;
		}

		#region Events

		/// <summary>
		/// Reports static property changes
		/// </summary>
		public static event EventHandler<PropertyChangedEventArgs> StaticPropertyChanged;

		/// <summary>
		/// Fires a new StaticPropertyChanged event
		/// </summary>
		/// <param name="propertyName"></param>
		private static void OnStaticPropertyChanged( string propertyName )
		{
			if ( StaticPropertyChanged != null )
			{
				StaticPropertyChanged( null, new PropertyChangedEventArgs( propertyName ) );
			}
		}

		#endregion Events

		private Library()
		{
			State = RemoteFileObjectState.Initializing;

			m_entries = new Dictionary<long, LibraryEntry>();
			Playlists = new SuperCollection<Playlist>();

			Playlists.ItemAdded += Playlists_ItemAdded;
			Playlists.ItemRemoved += Playlists_ItemRemoved;
			Playlists.ItemReplaced += Playlists_ItemReplaced;
			Playlists.ItemsCleared += Playlists_ItemsCleared;
		}

		/// <summary>
		/// Creates and initializes the singleton instance of this class
		/// </summary>
		public static void Init()
		{
			if ( s_instance == null )
			{
				s_instance = new Library();

				s_instance.ScheduleInit();

				OnStaticPropertyChanged( "Instance" );
			}
			else
			{
				// TODO: exception
			}
		}

		/// <summary>
		/// Schedules the asynchronous initialization of this file object
		/// </summary>
		private void ScheduleInit()
		{
			lock ( m_lock )
			{
				// TODO: az egész legyen aszinkron
				try
				{
					LoadLibrary();

					if ( Synchronizable )
					{
						ScheduleSynchronize();
					}
					else
					{
						State = RemoteFileObjectState.Local;
					}

					Register();
				}
				catch ( Exception e )
				{
					State = RemoteFileObjectState.Error;
					Error = e;
				}
			}
		}

		/// <summary>
		/// Loads the items of the library from the local library location
		/// </summary>
		private void LoadLibrary()
		{
			LoadEntries();
			LoadPlaylistList();
		}

		/// <summary>
		/// Loads the library entries from disk
		/// </summary>
		private void LoadEntries()
		{
			// TODO: LoadEntries()
		}

		/// <summary>
		/// Loads the list of playlist files from disk. Playlists can be loaded individually.
		/// </summary>
		private void LoadPlaylistList()
		{
			lock ( m_lock )
			{
				m_loadingPlaylists = true;
				try
				{
					var regexPlaylists = RegexUtils.MatchEndWithAny( PlaylistIO.PlaylistFileExtensions );

					List<Playlist> toKeep = new List<Playlist>();

					// TODO: ne így generálja az útvonalat, legyen külön property registryben; path converter függvények is lehetnének ott
					foreach ( var playlistFile in ServiceCenter.FileHandler.GetFiles( Registry.ToFullPath( CloudManager.PlaylistsDir, Registry.PathKind.LocalUser ) ) )
					{
						if ( regexPlaylists.IsMatch( playlistFile ) )
						{
							Uri playlistFileUri = new Uri( playlistFile );

							var playlist = (from pls
											in Playlists
											where pls.PlaylistFile != null && pls.PlaylistFile.FilePath == playlistFileUri
											select pls).FirstOrDefault();

							if ( playlist == null )
							{
								playlist = new PlaylistFile( null, playlistFileUri, LocalPlaylistPathToRemote( playlistFile ), CloudConnection, true, true ).RepresentedObject;
								Playlists.Add( playlist );
							}

							toKeep.Add( playlist );
						}
					}

					Playlists.RemoveRange( Playlists.Except( toKeep ) );
				}
				finally
				{
					m_loadingPlaylists = false;
				}
			}
		}

		/// <summary>
		/// Saves the list of playlists to disk if needed and schedules their synchronization if possible.
		/// The saving of the actual playlists happens individually
		/// </summary>
		/// <param name="added">The list of playlists added that may need creating on disk. If both this and "removed" is null, a full sync will be done.</param>
		/// <param name="removed">The list of playlists removed that may need deleting from disk. If both this and "added" is null, a full sync will be done.</param>
		private void SavePlaylistListIfNeeded( IEnumerable<Playlist> added = null, IEnumerable<Playlist> removed = null )
		{
			lock ( m_lock )
			{
				if ( !m_loadingPlaylists )
				{
					DoSavePlaylistList( added, removed );

					if ( Synchronizable )
					{
						ScheduleSynchronize();
					}
				}
			}
		}

		// TODO: collectionChange-re elmenteni a playlistek megjelenítési sorrendjét

		/// <summary>
		/// Do the actual saving of the list of playlists
		/// </summary>
		/// <param name="added">The list of playlists added that may need creating on disk. If both this and "removed" is null, a full sync will be done.</param>
		/// <param name="removed">The list of playlists removed that may need deleting from disk. If both this and "added" is null, a full sync will be done.</param>
		private void DoSavePlaylistList( IEnumerable<Playlist> added, IEnumerable<Playlist> removed )
		{
			lock ( m_lock )
			{
				if ( added == null && removed == null )
				{
					List<Uri> toKeep = new List<Uri>();

					foreach ( var playlist in Playlists )
					{
						if ( playlist.PlaylistFile == null )
						{
							playlist.PlaylistFile = CreatePlaylistFileFor( playlist ); // this saves it automatically
						}
						else
						{
							playlist.PlaylistFile.Save();
						}

						toKeep.Add( playlist.PlaylistFile.FilePath );
					}

					var regexPlaylists = RegexUtils.MatchEndWithAny( PlaylistIO.PlaylistFileExtensions );

					foreach ( var playlistFile in ServiceCenter.FileHandler.GetFiles( Registry.ToFullPath( CloudManager.PlaylistsDir, Registry.PathKind.LocalUser ) ) )
					{
						if ( regexPlaylists.IsMatch( playlistFile ) )
						{
							Uri playlistFileUri = new Uri( playlistFile );

							if ( !toKeep.Contains( playlistFileUri ) )
							{
								ServiceCenter.FileHandler.MarkDeleted( playlistFile );
							}
						}
					}
				}
				else
				{
					if ( added != null )
					{
						foreach ( var item in added )
						{
							if ( item.PlaylistFile == null )
							{
								item.PlaylistFile = CreatePlaylistFileFor( item ); // this saves it automatically
							}
							else
							{
								item.PlaylistFile.Save();
							}
						}
					}

					if ( removed != null )
					{
						foreach ( var item in removed )
						{
							if ( item.PlaylistFile != null )
							{
								ServiceCenter.FileHandler.MarkDeleted( item.PlaylistFile.FilePath.LocalPath );
							}
						}
					}
				}
			}
		}

		/// <summary>
		/// Registers this object in the cloud manager. Call Dispose() to unregister.
		/// </summary>
		private void Register()
		{
			lock ( m_lock )
			{
				if ( CloudConnection != null )
				{
					CloudManager.Instance.CloudObjects.Add( this );
				}
			}
		}

		private void synchEntriesRequest_RequestExecutionStarted( object sender, CloudRequestStartedEventArgs e )
		{
			lock ( m_lock )
			{
				m_synchEntriesRequestScheduled = false;
			}
		}

		private void synchEntriesRequest_RequestCanceled( object sender, CloudRequestCanceledEventArgs e )
		{
			lock ( m_lock )
			{
				m_synchEntriesRequestScheduled = false;
			}
		}

		private void synchPlaylistsRequest_RequestExecutionStarted( object sender, CloudRequestStartedEventArgs e )
		{
			lock ( m_lock )
			{
				m_synchPlaylistsRequest.RequestCanceled -= synchPlaylistsRequest_RequestCanceled;
				m_synchPlaylistsRequest.RequestExecutionStarted -= synchPlaylistsRequest_RequestExecutionStarted;
				m_synchPlaylistsRequest.RequestErrored -= synchRequest_RequestErrored;
				m_synchPlaylistsRequest.RequestSucceeded -= synchRequest_RequestSucceeded;

				m_synchPlaylistsRequest = null;
			}
		}

		private void synchPlaylistsRequest_RequestCanceled( object sender, CloudRequestCanceledEventArgs e )
		{
			lock ( m_lock )
			{
				m_synchPlaylistsRequest.RequestCanceled -= synchPlaylistsRequest_RequestCanceled;
				m_synchPlaylistsRequest.RequestExecutionStarted -= synchPlaylistsRequest_RequestExecutionStarted;
				m_synchPlaylistsRequest.RequestErrored -= synchRequest_RequestErrored;
				m_synchPlaylistsRequest.RequestSucceeded -= synchRequest_RequestSucceeded;

				m_synchPlaylistsRequest = null;
			}
		}

		private void synchRequest_RequestSucceeded( object sender, CloudRequestSucceededEventArgs e )
		{
			lock ( m_lock )
			{
				State = RemoteFileObjectState.Remote;
			}
		}

		private void synchRequest_RequestErrored( object sender, CloudRequestErroredEventArgs e )
		{
			lock ( m_lock )
			{
				State = RemoteFileObjectState.Error;
				Error = e.Cause;
			}
		}

		private void Playlists_ItemAdded( object sender, General.Events.CollectionItemAddedEventArgs e )
		{
			SavePlaylistListIfNeeded( added: new Playlist[] { (Playlist) e.Item } );
		}

		private void Playlists_ItemRemoved( object sender, General.Events.CollectionItemRemovedEventArgs e )
		{
			// remove items from the cloud manager sync list as well
			SavePlaylistListIfNeeded( removed: new Playlist[] { (Playlist) e.Item } );

			if ( ((Playlist) e.Item).PlaylistFile != null )
			{
				((Playlist) e.Item).PlaylistFile.Dispose();
				((Playlist) e.Item).PlaylistFile = null;
			}
		}

		private void Playlists_ItemReplaced( object sender, General.Events.CollectionItemReplacedEventArgs e )
		{
			// remove items from the cloud manager sync list as well
			SavePlaylistListIfNeeded( added: new Playlist[] { (Playlist) e.NewItem }, removed: new Playlist[] { (Playlist) e.OldItem } );

			if ( ((Playlist) e.OldItem).PlaylistFile != null )
			{
				((Playlist) e.OldItem).PlaylistFile.Dispose();
				((Playlist) e.OldItem).PlaylistFile = null;
			}
		}

		private void Playlists_ItemsCleared( object sender, General.Events.CollectionItemsClearedEventArgs e )
		{
			// remove items from the cloud manager sync list as well
			SavePlaylistListIfNeeded( removed: e.Items.OfType<Playlist>() );

			foreach ( var item in e.Items )
			{
				if ( ((Playlist) item).PlaylistFile != null )
				{
					((Playlist) item).PlaylistFile.Dispose();
					((Playlist) item).PlaylistFile = null;
				}
			}
		}

		#region ISynchronizable Members

		/// <summary>
		/// Object providing the connection to the cloud storing this object
		/// </summary>
		public CloudConnectionBase CloudConnection
		{
			get
			{
				return DefaultCloudConnectionProxy.Instance;
			}
		}

		private Exception m_error;

		/// <summary>
		/// The last error that occured while managing this file or null if none occured yet
		/// </summary>
		public Exception Error // TODO: írja ki valahol
		{
			get
			{
				lock ( m_lock )
				{
					return m_error;
				}
			}

			protected set
			{
				lock ( m_lock )
				{
					if ( m_error != value )
					{
						m_error = value;
						OnPropertyChanged( new PropertyChangedEventArgs( "Error" ) );
					}
				}
			}
		}

		private RemoteFileObjectState m_state;

		/// <summary>
		/// The current state of readyness of this object or the current operation being performed on it
		/// </summary>
		public RemoteFileObjectState State
		{
			get
			{
				lock ( m_lock )
				{
					return m_state;
				}
			}

			private set
			{
				lock ( m_lock )
				{
					if ( m_state != value )
					{
						m_state = value;
						OnPropertyChanged( new PropertyChangedEventArgs( "State" ) );
					}
				}
			}
		}

		/// <summary>
		/// Can the item be synchronized currently, is it actually remote
		/// or just a local file stored as a remote file object
		/// </summary>
		public bool Synchronizable
		{
			get
			{
				lock ( m_lock )
				{
					return CloudConnection != null && CloudConnection.IsAvailable;
				}
			}
		}

		/// <summary>
		/// Schedules the (asynchronous) synchronization of this object with the one stored in the cloud
		/// </summary>
		public void ScheduleSynchronize()
		{
			ScheduleSynchronizeEntries();
			ScheduleSynchronizePlaylistList();
		}

		#endregion ISynchronizable Members

		/// <summary>
		/// Schedules the (asynchronous) synchronization of the library entries with the cloud
		/// </summary>
		public void ScheduleSynchronizeEntries()
		{
			// TODO: ScheduleSynchronizeEntries() - úgy, mint a ScheduleSynchronizePlaylistList
			lock ( m_lock )
			{
				if ( !Synchronizable )
				{
					throw new InvalidOperationException( "The item is not synchronizable now." );
				}

				if ( m_synchEntriesRequestScheduled )
				{
					return;
				}

				//string tempPath = ServiceCenter.FileHandler.GetTempFileName();
				//ulong lockId = RequestFactory.GenerateLockId();

				//var request = RequestFactory.MakeFindRequest( CloudManager.LibraryIndexDir, RegexUtils.MatchEndWithAny( LibraryIO.LibraryFileExtensions ), true, async ( libraryFiles, conn ) =>
				//		{

				//		} );

				//request.RequestCanceled += synchEntriesRequest_RequestCanceled;
				//request.RequestExecutionStarted += synchEntriesRequest_RequestExecutionStarted;
				//request.RequestErrored += synchRequest_RequestErrored;
				//request.RequestSucceeded += synchRequest_RequestSucceeded;

				m_synchEntriesRequestScheduled = true;

				//CloudManager.Instance.Schedule( request, CloudConnection );
			}
		}

		/// <summary>
		/// Schedules the (asynchronous) synchronization of the list of playlists in the library with the cloud
		/// </summary>
		public void ScheduleSynchronizePlaylistList()
		{
			lock ( m_lock )
			{
				if ( !Synchronizable )
				{
					throw new InvalidOperationException( "The item is not synchronizable now." );
				}

				if ( m_synchPlaylistsRequest != null )
				{
					return;
				}

				string tempPath = ServiceCenter.FileHandler.GetTempFileName();
				ulong lockId = RequestFactory.GenerateLockId();

				m_synchPlaylistsRequest = RequestFactory.MakeFindRequest( CloudManager.PlaylistsDir, RegexUtils.MatchEndWithAny( PlaylistIO.PlaylistFileExtensions ), false, async ( playlistFiles, conn ) =>
						{
							foreach ( var playlistFile in playlistFiles )
							{
								string localPath = RemotePlaylistPathToLocal( playlistFile );
								FileMetadata localFileDeletionMetadata = ServiceCenter.FileHandler.GetDeletionMetadata( localPath );
								DateTime remoteFileModified = (await conn.GetMetadata( playlistFile )).LastWriteTimeUtc ?? DateTime.MinValue;

								// if it hasn't been marked as deleted locally or it has been earlier than the last change to the remote file
								// and the local file doesn't exist
								if ( (localFileDeletionMetadata == null || localFileDeletionMetadata.LastWriteTimeUtc < remoteFileModified)
									&& !ServiceCenter.FileHandler.Exists( localPath ) )
								{
									await conn.Get( playlistFile, new Uri( localPath, UriKind.Absolute ) );
								}
								else
								{
									if ( localFileDeletionMetadata != null )
									{
										await conn.MarkDeleted( playlistFile );
									}

									// otherwise the file object should solve this by itself
								}
							}

							var regexRemotes = RegexUtils.MatchEndWithAny( playlistFiles );

							foreach ( var playlistFile in ServiceCenter.FileHandler.GetFiles( Registry.ToFullPath( CloudManager.PlaylistsDir, Registry.PathKind.LocalUser ) ) )
							{
								if ( !regexRemotes.IsMatch( playlistFile ) )
								{
									string remotePath = LocalPlaylistPathToRemote( playlistFile );
									FileMetadata remoteFileDeletionMetadata = await conn.GetDeletionMetadata( remotePath );
									DateTime localFileModified = ServiceCenter.FileHandler.GetMetadata( playlistFile ).LastWriteTimeUtc ?? DateTime.MinValue;

									// if it hasn't been marked as deleted from the cloud or it has been earlier than the last change to the local file
									// and the remote file doesn't exist
									if ( (remoteFileDeletionMetadata == null || remoteFileDeletionMetadata.LastWriteTimeUtc < localFileModified)
										&& !await conn.Exists( remotePath ) )
									{
										await conn.Put( remotePath, new Uri( playlistFile, UriKind.Absolute ) );
									}
									else
									{
										if ( remoteFileDeletionMetadata != null )
										{
											ServiceCenter.FileHandler.MarkDeleted( playlistFile );
										}

										// otherwise the file object should solve this by itself
									}
								}
							}

							LoadPlaylistList();
						} );

				m_synchPlaylistsRequest.RequestCanceled += synchPlaylistsRequest_RequestCanceled;
				m_synchPlaylistsRequest.RequestExecutionStarted += synchPlaylistsRequest_RequestExecutionStarted;
				m_synchPlaylistsRequest.RequestErrored += synchRequest_RequestErrored;
				m_synchPlaylistsRequest.RequestSucceeded += synchRequest_RequestSucceeded;

				CloudManager.Instance.Schedule( m_synchPlaylistsRequest, CloudConnection );
			}
		}

		#region Utility methods

		/// <summary>
		/// Converts the passed absolute path of a local in-library playlist to its counterpart in the cloud
		/// </summary>
		/// <param name="localPath"></param>
		/// <returns></returns>
		private string LocalPlaylistPathToRemote( string localPath )
		{
			string prefix = Registry.GetBasePathOf( Registry.PathKind.LocalUser );

			if ( localPath.StartsWith( prefix ) )
			{
				return localPath.Substring( prefix.Length ).Replace( '\\', '/' );
			}
			else
			{
				throw new InvalidOperationException( "The passed path doesn't look like a valid remote library path: " + localPath );
			}
		}

		/// <summary>
		/// Converts the passed absolute path of a remote in-library playlist to its counterpart in the local library directory
		/// </summary>
		/// <param name="remotePath"></param>
		/// <returns></returns>
		private string RemotePlaylistPathToLocal( string remotePath )
		{
			return Registry.ToFullPath( remotePath, Registry.PathKind.LocalUser );
		}

		/// <summary>
		/// Creates a playlist file for the passed playlist
		/// </summary>
		/// <param name="playlist"></param>
		/// <returns></returns>
		private PlaylistFile CreatePlaylistFileFor( Playlist playlist )
		{
			string remotePath = string.Format( "{0}/{1}{2}", CloudManager.PlaylistsDir, "bdpl_" + DateTime.UtcNow.ToString( "yyyy-MM-dd_HH-mm-ss-fff" ), XplfIO.FileExtension );

			return new PlaylistFile( playlist, new Uri( Registry.ToFullPath( remotePath, Registry.PathKind.LocalUser ) ), remotePath, CloudConnection, true, true );
		}

		#endregion Utility methods

		#region IDisposable Members

		/// <summary>
		/// Removes this file object from where it has registered itself and cancels its async processes
		/// </summary>
		public void Dispose()
		{
			lock ( m_lock )
			{
				CloudManager.Instance.CloudObjects.Remove( this );
			}
		}

		#endregion IDisposable Members

		#region INotifyPropertyChanged Members

		/// <summary>
		/// Event fired when one of the object's properties changes
		/// </summary>
		public event PropertyChangedEventHandler PropertyChanged;

		#endregion INotifyPropertyChanged Members

		/// <summary>
		/// Fires a new PropertyChanged event
		/// </summary>
		/// <param name="e"></param>
		protected virtual void OnPropertyChanged( PropertyChangedEventArgs e )
		{
			if ( PropertyChanged != null )
			{
				PropertyChanged( this, e );
			}
		}
	}
}
